/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sanselan.formats.icns;

import java.io.IOException;
import java.util.ArrayList;

import org.apache.sanselan.ImageReadException;

import com.jgraph.gaeawt.java.awt.image.BufferedImage;

public class IcnsDecoder
{
	private static final int[] palette_4bpp =
	{
		0xffffffff,
		0xfffcf305,
		0xffff6402,
		0xffdd0806,
		0xfff20884,
		0xff4600a5,
		0xff0000d4,
		0xff02abea,
		0xff1fb714,
		0xff006411,
		0xff562c05,
		0xff90713a,
		0xffc0c0c0,
		0xff808080,
		0xff404040,
		0xff000000
	};

	private static final int[] palette_8bpp =
	{
		0xFFFFFFFF,
		0xFFFFFFCC,
		0xFFFFFF99,
		0xFFFFFF66,
		0xFFFFFF33,
		0xFFFFFF00,
		0xFFFFCCFF,
		0xFFFFCCCC,
		0xFFFFCC99,
		0xFFFFCC66,
		0xFFFFCC33,
		0xFFFFCC00,
		0xFFFF99FF,
		0xFFFF99CC,
		0xFFFF9999,
		0xFFFF9966,
		0xFFFF9933,
		0xFFFF9900,
		0xFFFF66FF,
		0xFFFF66CC,
		0xFFFF6699,
		0xFFFF6666,
		0xFFFF6633,
		0xFFFF6600,
		0xFFFF33FF,
		0xFFFF33CC,
		0xFFFF3399,
		0xFFFF3366,
		0xFFFF3333,
		0xFFFF3300,
		0xFFFF00FF,
		0xFFFF00CC,
		0xFFFF0099,
		0xFFFF0066,
		0xFFFF0033,
		0xFFFF0000,
		0xFFCCFFFF,
		0xFFCCFFCC,
		0xFFCCFF99,
		0xFFCCFF66,
		0xFFCCFF33,
		0xFFCCFF00,
		0xFFCCCCFF,
		0xFFCCCCCC,
		0xFFCCCC99,
		0xFFCCCC66,
		0xFFCCCC33,
		0xFFCCCC00,
		0xFFCC99FF,
		0xFFCC99CC,
		0xFFCC9999,
		0xFFCC9966,
		0xFFCC9933,
		0xFFCC9900,
		0xFFCC66FF,
		0xFFCC66CC,
		0xFFCC6699,
		0xFFCC6666,
		0xFFCC6633,
		0xFFCC6600,
		0xFFCC33FF,
		0xFFCC33CC,
		0xFFCC3399,
		0xFFCC3366,
		0xFFCC3333,
		0xFFCC3300,
		0xFFCC00FF,
		0xFFCC00CC,
		0xFFCC0099,
		0xFFCC0066,
		0xFFCC0033,
		0xFFCC0000,
		0xFF99FFFF,
		0xFF99FFCC,
		0xFF99FF99,
		0xFF99FF66,
		0xFF99FF33,
		0xFF99FF00,
		0xFF99CCFF,
		0xFF99CCCC,
		0xFF99CC99,
		0xFF99CC66,
		0xFF99CC33,
		0xFF99CC00,
		0xFF9999FF,
		0xFF9999CC,
		0xFF999999,
		0xFF999966,
		0xFF999933,
		0xFF999900,
		0xFF9966FF,
		0xFF9966CC,
		0xFF996699,
		0xFF996666,
		0xFF996633,
		0xFF996600,
		0xFF9933FF,
		0xFF9933CC,
		0xFF993399,
		0xFF993366,
		0xFF993333,
		0xFF993300,
		0xFF9900FF,
		0xFF9900CC,
		0xFF990099,
		0xFF990066,
		0xFF990033,
		0xFF990000,
		0xFF66FFFF,
		0xFF66FFCC,
		0xFF66FF99,
		0xFF66FF66,
		0xFF66FF33,
		0xFF66FF00,
		0xFF66CCFF,
		0xFF66CCCC,
		0xFF66CC99,
		0xFF66CC66,
		0xFF66CC33,
		0xFF66CC00,
		0xFF6699FF,
		0xFF6699CC,
		0xFF669999,
		0xFF669966,
		0xFF669933,
		0xFF669900,
		0xFF6666FF,
		0xFF6666CC,
		0xFF666699,
		0xFF666666,
		0xFF666633,
		0xFF666600,
		0xFF6633FF,
		0xFF6633CC,
		0xFF663399,
		0xFF663366,
		0xFF663333,
		0xFF663300,
		0xFF6600FF,
		0xFF6600CC,
		0xFF660099,
		0xFF660066,
		0xFF660033,
		0xFF660000,
		0xFF33FFFF,
		0xFF33FFCC,
		0xFF33FF99,
		0xFF33FF66,
		0xFF33FF33,
		0xFF33FF00,
		0xFF33CCFF,
		0xFF33CCCC,
		0xFF33CC99,
		0xFF33CC66,
		0xFF33CC33,
		0xFF33CC00,
		0xFF3399FF,
		0xFF3399CC,
		0xFF339999,
		0xFF339966,
		0xFF339933,
		0xFF339900,
		0xFF3366FF,
		0xFF3366CC,
		0xFF336699,
		0xFF336666,
		0xFF336633,
		0xFF336600,
		0xFF3333FF,
		0xFF3333CC,
		0xFF333399,
		0xFF333366,
		0xFF333333,
		0xFF333300,
		0xFF3300FF,
		0xFF3300CC,
		0xFF330099,
		0xFF330066,
		0xFF330033,
		0xFF330000,
		0xFF00FFFF,
		0xFF00FFCC,
		0xFF00FF99,
		0xFF00FF66,
		0xFF00FF33,
		0xFF00FF00,
		0xFF00CCFF,
		0xFF00CCCC,
		0xFF00CC99,
		0xFF00CC66,
		0xFF00CC33,
		0xFF00CC00,
		0xFF0099FF,
		0xFF0099CC,
		0xFF009999,
		0xFF009966,
		0xFF009933,
		0xFF009900,
		0xFF0066FF,
		0xFF0066CC,
		0xFF006699,
		0xFF006666,
		0xFF006633,
		0xFF006600,
		0xFF0033FF,
		0xFF0033CC,
		0xFF003399,
		0xFF003366,
		0xFF003333,
		0xFF003300,
		0xFF0000FF,
		0xFF0000CC,
		0xFF000099,
		0xFF000066,
		0xFF000033,
		0xFFEE0000,
		0xFFDD0000,
		0xFFBB0000,
		0xFFAA0000,
		0xFF880000,
		0xFF770000,
		0xFF550000,
		0xFF440000,
		0xFF220000,
		0xFF110000,
		0xFF00EE00,
		0xFF00DD00,
		0xFF00BB00,
		0xFF00AA00,
		0xFF008800,
		0xFF007700,
		0xFF005500,
		0xFF004400,
		0xFF002200,
		0xFF001100,
		0xFF0000EE,
		0xFF0000DD,
		0xFF0000BB,
		0xFF0000AA,
		0xFF000088,
		0xFF000077,
		0xFF000055,
		0xFF000044,
		0xFF000022,
		0xFF000011,
		0xFFEEEEEE,
		0xFFDDDDDD,
		0xFFBBBBBB,
		0xFFAAAAAA,
		0xFF888888,
		0xFF777777,
		0xFF555555,
		0xFF444444,
		0xFF222222,
		0xFF111111,
		0xFF000000
	};

	private static void decode1BPPImage(IcnsType imageType, byte[] imageData,
			BufferedImage bufferedImage)
	{
		int position = 0;
		int bitsLeft = 0;
		int value = 0;
		for (int y = 0; y < imageType.getHeight(); y++)
		{
			for (int x = 0; x < imageType.getWidth(); x++)
			{
				if (bitsLeft == 0)
				{
					value = 0xff & imageData[position++];
					bitsLeft = 8;
				}
				int argb;
				if ((value & 0x80) != 0)
					argb = 0xff000000;
				else
					argb = 0xffffffff;
				value <<= 1;
				bitsLeft--;
				bufferedImage.setRGB(x, y, argb);
			}
		}
	}

	private static void decode4BPPImage(IcnsType imageType, byte[] imageData,
			BufferedImage bufferedImage)
	{
		int i = 0;
		boolean visited = false;
		for (int y = 0; y < imageType.getHeight(); y++)
		{
			for (int x = 0; x < imageType.getWidth(); x++)
			{
				int index;
				if (!visited)
					index = 0xf & (imageData[i] >> 4);
				else
					index = 0xf & imageData[i++];
				visited = !visited;
				bufferedImage.setRGB(x, y, palette_4bpp[index]);
			}
		}
	}

	private static void decode8BPPImage(IcnsType imageType, byte[] imageData,
			BufferedImage bufferedImage)
	{
		for (int y = 0; y < imageType.getHeight(); y++)
		{
			for (int x = 0; x < imageType.getWidth(); x++)
			{
				int index = 0xff & imageData[y*imageType.getWidth() + x];
				bufferedImage.setRGB(x, y, palette_8bpp[index]);
			}
		}
	}

	private static void decode32BPPImage(IcnsType imageType, byte[] imageData,
			BufferedImage bufferedImage)
	{
		for (int y = 0; y < imageType.getHeight(); y++)
		{
			for (int x = 0; x < imageType.getWidth(); x++)
			{
				int argb = 0xff000000 /* the "alpha" is ignored */ |
						((0xff & imageData[4*(y*imageType.getWidth() + x) + 1]) << 16) |
						((0xff & imageData[4*(y*imageType.getWidth() + x) + 2]) << 8) |
						(0xff & imageData[4*(y*imageType.getWidth() + x) + 3]);
				bufferedImage.setRGB(x, y, argb);
			}
		}
	}

	private static void apply1BPPMask(byte[] maskData, BufferedImage bufferedImage) throws ImageReadException
	{
		int position = 0;
		int bitsLeft = 0;
		int value = 0;

		// 1 bit icon types have image data followed by mask data in the same entry
		int totalBytes = (bufferedImage.getWidth() * bufferedImage.getHeight() + 7) / 8;
		if (maskData.length >= 2*totalBytes)
			position = totalBytes;
		else
			throw new ImageReadException("1 BPP mask underrun parsing ICNS file");

		for (int y = 0; y < bufferedImage.getHeight(); y++)
		{
			for (int x = 0; x < bufferedImage.getWidth(); x++)
			{
				if (bitsLeft == 0)
				{
					value = 0xff & maskData[position++];
					bitsLeft = 8;
				}
				int alpha;
				if ((value & 0x80) != 0)
					alpha = 0xff;
				else
					alpha = 0x00;
				value <<= 1;
				bitsLeft--;
				bufferedImage.setRGB(x, y, (alpha << 24) |
					(0xffffff & bufferedImage.getRGB(x, y)));
			}
		}
	}

	private static void apply8BPPMask(byte[] maskData, BufferedImage bufferedImage)
	{
		for (int y = 0; y < bufferedImage.getHeight(); y++)
		{
			for (int x = 0; x < bufferedImage.getWidth(); x++)
			{
				int alpha = 0xff & maskData[y*bufferedImage.getWidth() + x];
				bufferedImage.setRGB(x, y, (alpha << 24) |
					(0xffffff & bufferedImage.getRGB(x, y)));
			}
		}
	}

	public static ArrayList decodeAllImages(IcnsImageParser.IcnsElement[] icnsElements)
			throws ImageReadException, IOException
	{
		ArrayList result = new ArrayList();
		for (int i = 0; i < icnsElements.length; i++)
		{
			IcnsImageParser.IcnsElement imageElement = icnsElements[i];
			IcnsType imageType = IcnsType.findImageType(imageElement.type);
			if (imageType == null)
				continue;

			IcnsType maskType = null;
			IcnsImageParser.IcnsElement maskElement = null;
			if (imageType.hasMask())
			{
				maskType = imageType;
				maskElement = imageElement;
			}
			else
			{
				maskType = IcnsType.find8BPPMaskType(imageType);
				if (maskType != null)
				{
					for (int j = 0; j < icnsElements.length; j++)
					{
						if (icnsElements[j].type == maskType.getType())
						{
							maskElement = icnsElements[j];
							break;
						}
					}
				}
				if (maskElement == null)
				{
					maskType = IcnsType.find1BPPMaskType(imageType);
					if (maskType != null)
					{
						for (int j = 0; j < icnsElements.length; j++)
						{
							if (icnsElements[j].type == maskType.getType())
							{
								maskElement = icnsElements[j];
								break;
							}
						}
					}
				}
			}

			// FIXME: don't skip these when JPEG 2000 support is added:
			if (imageType == IcnsType.ICNS_256x256_32BIT_ARGB_IMAGE ||
				imageType == IcnsType.ICNS_512x512_32BIT_ARGB_IMAGE)
				continue;

			int expectedSize = (imageType.getWidth()*imageType.getHeight()*
					imageType.getBitsPerPixel() + 7) / 8;
			byte[] imageData;
			if (imageElement.data.length < expectedSize)
			{
				if (imageType.getBitsPerPixel() == 32)
				{
					imageData = Rle24Compression.decompress(imageType.getWidth(),
							imageType.getHeight(), imageElement.data);
				}
				else
					throw new ImageReadException(
							"Short image data but not a 32 bit compressed type");
			}
			else
				imageData = imageElement.data;

			BufferedImage bufferedImage = new BufferedImage(imageType.getWidth(),
					imageType.getHeight(), BufferedImage.TYPE_INT_ARGB);
			switch (imageType.getBitsPerPixel())
			{
				case 1:
					decode1BPPImage(imageType, imageData, bufferedImage);
					break;
				case 4:
					decode4BPPImage(imageType, imageData, bufferedImage);
					break;
				case 8:
					decode8BPPImage(imageType, imageData, bufferedImage);
					break;
				case 32:
					decode32BPPImage(imageType, imageData, bufferedImage);
					break;
				default:
					throw new ImageReadException(
						"Unsupported bit depth " + imageType.getBitsPerPixel());
			}

			if (maskElement != null)
			{
				if (maskType.getBitsPerPixel() == 1)
					apply1BPPMask(maskElement.data, bufferedImage);
				else if (maskType.getBitsPerPixel() == 8)
					apply8BPPMask(maskElement.data, bufferedImage);
				else
					throw new ImageReadException("Unsupport mask bit depth " +
							maskType.getBitsPerPixel());
			}

			result.add(bufferedImage);
		}
		return result;
	}
}